/*
 * XQOptions.java
 */

package xq.ide;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.lang.annotation.Annotation;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;
import javax.xml.bind.annotation.XmlRootElement;
import org.apache.log4j.Logger;
import xq.util.URLClassLoader;
import static xq.util.LoggerUtil.getClassLogger;

/**
 *
 * @author ThorntonRP
 */
public class XQOptions {

	private static final Logger logger = getClassLogger();
	private static final XQOptions instance = new XQOptions();
	
	private final Preferences preferences;
	private final PropertyChangeSupport propertyChangeSupport;

	private XQOptions() {
		preferences = Preferences.userNodeForPackage(getClass());
		propertyChangeSupport = new PropertyChangeSupport(this);
	}

	public static XQOptions getInstance() {
		return instance;
	}

	public String getPreference(String key, String defaultValue) {
		return preferences.get(key, defaultValue);
	}

	public File getPreference(String key, File defaultValue) {
		return new File(preferences.get(key, defaultValue.getPath()));
	}

	public void setPreference(String key, String value) {
		preferences.put(key, value);
	}
	
	public void setConnectionPreferences(List<ConnectionInfo> connectionList) {
		try {
			for (String key : preferences.keys()) {
				if (key.matches("connection\\[\\d+\\]\\.\\w+")) {
					preferences.remove(key);
				}
			}
		} catch (BackingStoreException ex) {
			logger.error("Failed to reset connection preferences.", ex);
		}
		int index = 0;
		for (ConnectionInfo connectionInfo : connectionList) {
			preferences.put("connection[" + index + "].name", connectionInfo.getName());
			preferences.put("connection[" + index + "].userName", connectionInfo.getUserName());
			preferences.put("connection[" + index + "].password", connectionInfo.getPassword());
			preferences.put("connection[" + index + "].hostName", connectionInfo.getHostName());
			preferences.put("connection[" + index + "].portNumber", connectionInfo.getPortNumber());
			preferences.put("connection[" + index + "].uriPattern", connectionInfo.getURIPattern());
			index++;
		}
		propertyChangeSupport.firePropertyChange("connectionPreferences", null, connectionList);
	}

	public List<ConnectionInfo> getConnectionPreferences() {
		List<ConnectionInfo> connectionList = new ArrayList<ConnectionInfo>();
		ConnectionInfo connectionInfo = null;
		int index = 0;
		do {
			String connectionName = null;
			try {
				connectionName = preferences.get("connection[" + index + "].name", null);
				if (connectionName != null) {
					connectionInfo = new ConnectionInfo(
							preferences.get("connection[" + index + "].name", null),
							preferences.get("connection[" + index + "].userName", null),
							preferences.get("connection[" + index + "].password", null),
							preferences.get("connection[" + index + "].hostName", null),
							preferences.get("connection[" + index + "].portNumber", null),
							preferences.get("connection[" + index + "].uriPattern", null));
					connectionList.add(connectionInfo);
				} else {
					connectionInfo = null;
				}
			} catch (Exception ex) {
				logger.warn("Failed to load connection \"" + connectionName + "\": " + ex);
				connectionInfo = null;
			}
			index++;
		} while (connectionInfo != null);
		return connectionList;
	}

	public ClassLoader getBindingClassLoader() {
		return new URLClassLoader(getBindingClasspath(), ClassLoader.getSystemClassLoader());
	}

	public ClassLoader getResourceClassLoader() {
		return new URLClassLoader(getResourceClasspath());
	}
	
	public List<URL> getBindingClasspath() {
		return getClasspath("binding");
	}

	public void setBindingClasspath(List<URL> classpath) {
		setClasspath("binding", classpath);
	}

	public List<URL> getResourceClasspath() {
		return getClasspath("resource");
	}

	public void setResourceClasspath(List<URL> classpath) {
		setClasspath("resource", classpath);
	}
	
	public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
		propertyChangeSupport.addPropertyChangeListener(propertyName, listener);
	}

	public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) {
		propertyChangeSupport.removePropertyChangeListener(propertyName, listener);
	}

	private List<URL> getClasspath(String prefix) {
		List<URL> classpath = new ArrayList<URL>();
		String entry;
		int index = 0;
		do {
			entry = preferences.get(prefix + ".classpath.entry[" + index + "]", null);
			if (entry != null) {
				try {
					URL url = new URL(entry);
					classpath.add(url);
				} catch (MalformedURLException ex) {
					logger.warn("Invalid classpath entry \"" + entry + "\": " + ex, ex);
				}
			}
			index++;
		} while (entry != null);
		return classpath;
	}

	private void setClasspath(String prefix, List<URL> classpath) {
		try {
			for (String key : preferences.keys()) {
				if (key.matches(prefix + "\\.classpath\\.entry\\[\\d+\\]")) {
					preferences.remove(key);
				}
			}
		} catch (BackingStoreException ex) {
			logger.error("Failed to reset connection preferences.", ex);
		}
		int index = 0;
		for (URL entry : classpath) {
			preferences.put(prefix + ".classpath.entry[" + index + "]", entry.toString());
			index++;
		}
		propertyChangeSupport.firePropertyChange(prefix + "Classpath", null, classpath);
	}

	public List<String> getRootElementClassNames() {
		ClassLoader loader = getBindingClassLoader();
		List<String> classNames = new ArrayList<String>();
		try {
			List<File> files = toFiles(loader.getResources("."));
			for (File f : files) {
				filterForRootElementClasses(f.getPath(), f.listFiles(), classNames, loader);
			}
		} catch (Exception ex) {
			logger.warn(ex.toString(), ex);
		}
		return classNames;
	}

	@SuppressWarnings("unchecked")
	private void filterForRootElementClasses(String basePrefix, File[] candidates, List<String> results, ClassLoader loader) {
		if (candidates != null) {
			for (File f : candidates) {
				if (f.isDirectory()) {
					filterForRootElementClasses(basePrefix, f.listFiles(), results, loader);
				} else if (f.getName().endsWith(".class")) {
					String className = toClassName(basePrefix, f.getPath());
					try {
						Class c = loader.loadClass(className);
						Annotation a = c.getAnnotation(XmlRootElement.class);
						if (a != null) {
							results.add(className);
						}
					} catch (Throwable thrown) {
						logger.warn(thrown.toString(), thrown);
					}
				}
			}
		}
	}

	private String toClassName(String basePrefix, String pathName) {
		String relativePath = pathName.substring(basePrefix.length() + 1, pathName.length() - ".class".length());
		String className = relativePath.replace(File.separatorChar, '.');
		return className;
	}

	private List<File> toFiles(Enumeration<URL> e) {
		List<File> files = new ArrayList<File>();
		while (e.hasMoreElements()) {
			URL url = e.nextElement();
			if ("file".equals(url.getProtocol())) {
				try {
					files.add(new File(URLDecoder.decode(url.getFile(), "UTF-8")));
				} catch (UnsupportedEncodingException ex) {
					logger.warn(ex.toString(), ex);
				}
			}
		}
		return files;
	}
}
